package com.lagou.tx;

import com.lagou.DataSourceUtil;
import com.lagou.anno.Transactional;
import com.lagou.config.BeanProcess;
import com.lagou.util.ClassUtil;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import javax.sql.DataSource;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;

/**
 * @author: wudong
 * @create: 2021-05-31 17:27
 **/
public class SpringTractionManager implements BeanProcess {

    private DataSource dataSource;

    public SpringTractionManager(DataSource dataSource) {
        this.dataSource = dataSource;
    }


    @Override
    public Object postProcessAfterInitialization(Object bean, Class<?> orginClass){
        ClassMethodAnn<Transactional> build = ClassMethodAnn.build(orginClass, Transactional.class);
        if(!build.hasAnnotation()){
            return bean;
        }

        //开启事务

        Set<Class<?>> allInterFaces = getAllInterFaces(orginClass);
        if(allInterFaces != null && allInterFaces.size() > 0){
            return Proxy.newProxyInstance(orginClass.getClassLoader(),allInterFaces.toArray(new Class<?>[0]),new InvocationHandler(){
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    Transactional annotation = build.getAnnotation(method);

                    if(annotation == null){
                        return method.invoke(bean,args);
                    }

                    DataSourceUtil.ConnectionHolder connectionHolder = DataSourceUtil.getConnect(dataSource);
                    if(connectionHolder == null){
                        connectionHolder = getConnectionHolder();
                        DataSourceUtil.bindConnect(dataSource,connectionHolder);
                    }
                    try{
                        Object invoke = method.invoke(bean, args);
                        connectionHolder.getConect().commit();
                        return invoke;
                    }catch (Exception e){
                        connectionHolder.getConect().rollback();
                        throw new RuntimeException(e);
                    }finally {
                        DataSourceUtil.unBindConnect(dataSource);
                        connectionHolder.getConect().setAutoCommit(true);
                        connectionHolder.getConect().close();
                    }
                }
            });
        }else {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(orginClass);
            enhancer.setCallback(new MethodInterceptor() {
                @Override
                public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy)
                        throws Throwable {
                    Transactional annotation = method.getAnnotation(Transactional.class);
                    if(annotation == null){
                        return methodProxy.invoke(bean,args);
                    }

                    DataSourceUtil.ConnectionHolder connectionHolder = DataSourceUtil.getConnect(dataSource);
                    if(connectionHolder == null){
                        connectionHolder = getConnectionHolder();
                        DataSourceUtil.bindConnect(dataSource,connectionHolder);
                    }
                    try{
                        Object invoke = methodProxy.invoke(bean, args);
                        connectionHolder.getConect().commit();
                        return invoke;
                    }catch (Exception e){
                        connectionHolder.getConect().rollback();
                        throw new RuntimeException(e);
                    }finally {
                        DataSourceUtil.unBindConnect(dataSource);
                        connectionHolder.getConect().setAutoCommit(true);
                        connectionHolder.getConect().close();
                    }
                }
            });
            return enhancer.create();
        }

    }

    private DataSourceUtil.ConnectionHolder getConnectionHolder(){
        DataSourceUtil.ConnectionHolder connectionHolder = new DataSourceUtil.ConnectionHolder() {
            private Connection connection;
            @Override
            public Connection getConect() {
                //减小事务锁定时间
                try {
                    if(connection != null){
                        return connection;
                    }
                    connection = dataSource.getConnection();
                    connection.setAutoCommit(false);
                    return connection;
                } catch (SQLException e) {
                    throw new IllegalStateException("获取连接失败", e);
                }
            }
        };
        return connectionHolder;
    }

    private Set<Class<?>> getAllInterFaces(Class<?> clazz){
        Set<Class<?>> interfaces = new LinkedHashSet<>();
        Class<?> current = clazz;
        while (current != null) {
            Class<?>[] ifcs = current.getInterfaces();
            for (Class<?> ifc : ifcs) {
                interfaces.add(ifc);
            }
            current = current.getSuperclass();
        }
        if(clazz.isInterface()){
            interfaces.add(clazz);
        }
        return interfaces;
    }

    private static class ClassMethodAnn<T extends Annotation>{
        private Class clazz;
        private Map<MethodSign, T> methodAnnos = new HashMap<>();

        public T getAnnotation(Method method){
            MethodSign methodSign = new MethodSign();
            methodSign.setMethodName(method.getName());
            methodSign.setParameterTypes(method.getParameterTypes());

            T annotation = methodAnnos.get(methodSign);
            return annotation;
        }

        public boolean hasAnnotation(){
            return !methodAnnos.isEmpty();
        }

        public static <T extends Annotation> ClassMethodAnn build(Class clazz,Class<T> annotation){
            ClassMethodAnn<T> classMethodAnn = new ClassMethodAnn<T>();
            classMethodAnn.setClazz(clazz);

            T classAnnotation = (T)clazz.getAnnotation(annotation);
            Method[] declaredMethods = clazz.getDeclaredMethods();
            for (Method method : declaredMethods) {
                T methodAnnotation = method.getAnnotation(annotation);


                if(methodAnnotation == null && classAnnotation == null){
                    continue;
                }

                MethodSign methodSign = new MethodSign();
                methodSign.setMethodName(method.getName());
                methodSign.setParameterTypes(method.getParameterTypes());

                classMethodAnn.getMethodAnnos().put(methodSign,methodAnnotation == null ? classAnnotation : methodAnnotation);
            }

            List<Class> sups = new ArrayList<>();
            Consumer<Class<?>> classConsumer = new Consumer<Class<?>>() {
                @Override
                public void accept(Class<?> aClass) {
                    sups.add(aClass);
                }
            };
            ClassUtil.findAllClass(clazz,classConsumer);
            for (Class sup : sups) {
                ClassMethodAnn build = build(sup, annotation);
                classMethodAnn.merge(build);
            }
            return classMethodAnn;
        }

        public void merge(ClassMethodAnn<T> classMethodAnn){
            Map<MethodSign, T> methodAnnos = classMethodAnn.getMethodAnnos();
            Set<Map.Entry<MethodSign, T>> entries = methodAnnos.entrySet();
            for (Map.Entry<MethodSign, T> entry : entries) {
                this.methodAnnos.putIfAbsent(entry.getKey(),entry.getValue());
            }
        }

        public Class getClazz() {
            return clazz;
        }

        public void setClazz(Class clazz) {
            this.clazz = clazz;
        }

        public Map<MethodSign, T> getMethodAnnos() {
            return methodAnnos;
        }

        public void setMethodAnnos(Map<MethodSign, T> methodAnnos) {
            this.methodAnnos = methodAnnos;
        }
    }

    private static class MethodSign{
        private String methodName;

        private Class<?>[] parameterTypes;

        public String getMethodName() {
            return methodName;
        }

        public void setMethodName(String methodName) {
            this.methodName = methodName;
        }

        public Class<?>[] getParameterTypes() {
            return parameterTypes;
        }

        public void setParameterTypes(Class<?>[] parameterTypes) {
            this.parameterTypes = parameterTypes;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof MethodSign)) return false;
            MethodSign that = (MethodSign) o;
            return Objects.equals(methodName, that.methodName) &&
                    Arrays.equals(parameterTypes, that.parameterTypes);
        }

        @Override
        public int hashCode() {
            int result = Objects.hash(methodName);
            result = 31 * result + Arrays.hashCode(parameterTypes);
            return result;
        }
    }
}
